/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using System.ComponentModel;

namespace DaveSexton.DocProject.Sandcastle.TopicManagement
{
	[Flags]
	public enum InheritanceTopicFilterModes
	{
		None = 0, 

		Local = 1, 
		Remote = 2, 

		Prefixes = 4, 

		All = Local | Remote | Prefixes
	}

	/// <remarks>
	/// Matching API references are removed immediately; therefore, subsequent filters cannot be used to re-include those API references.  (Note that the APIs themselves are not removed, only nested references.)
	/// This behavior differs from the <see cref="RegexTopicFilter"/> and <see cref="CategoryTopicFilter"/> since they do not have any immediate affect on matching APIs.
	/// </remarks>
	[Serializable]
	[DisplayName("Inheritance Filter"), Description("Removes inherited member information from topics based on filter criteria.")]
	public sealed class InheritanceTopicFilter : TopicFilter
	{
		#region Public Properties
		public override string DisplayValue
		{
			get
			{
				return string.Format(System.Globalization.CultureInfo.CurrentCulture, Resources.Text.InheritanceFilterDisplayValue, 
					Environment.NewLine, Modes, Settings.Read("prefixes", "{none}"), IncludePrefixes);
			}
		}

		public override Type EditorType
		{
			get
			{
				return typeof(InheritanceTopicFilterControl);
			}
		}

		public InheritanceTopicFilterModes Modes
		{
			get
			{
				return Settings.Read("modes", InheritanceTopicFilterModes.All);
			}
			set
			{
				if (value != Modes)
				{
					Settings.Write("modes", value);

					OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("Modes"));
				}
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
		public ICollection<string> Prefixes
		{
			get
			{
				return new List<string>(Settings.Read<IEnumerable<string>>("prefixes", new List<string>(0))).AsReadOnly();
			}
			set
			{
				Settings.Write<IEnumerable<string>>("prefixes", value);

				OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("Prefixes"));
			}
		}

		public bool IncludePrefixes
		{
			get
			{
				return Settings.Read("includePrefixes", (Modes & (InheritanceTopicFilterModes.Local | InheritanceTopicFilterModes.Remote)) != 0);
			}
			set
			{
				if (value != IncludePrefixes)
				{
					Settings.Write("includePrefixes", value);

					OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("IncludePrefixes"));
				}
			}
		}
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="InheritanceTopicFilter" /> class.
		/// </summary>
		public InheritanceTopicFilter()
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="InheritanceTopicFilter" /> class.
		/// </summary>
		public InheritanceTopicFilter(InheritanceTopicFilterModes modes, ICollection<string> prefixes)
		{
			if (prefixes == null)
				throw new ArgumentNullException("prefixes");

			Modes = modes;
			Prefixes = prefixes;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="InheritanceTopicFilter" /> class.
		/// </summary>
		public InheritanceTopicFilter(InheritanceTopicFilter clone)
			: base(clone)
		{
			Modes = clone.Modes;
			Prefixes = clone.Prefixes;
		}
		#endregion

		#region Methods
		public override bool IsMatch(Topic topic)
		{
			// This overload will be called by the topic management dialog when the Apply button is clicked, 
			// but this filter never matches any topics - it simply edits their reflection information in place, 
			// which is unncessary while managing topics.
			return false;
		}

		public override bool IsMatch(ReflectionApiElement element)
		{
			if (!Include)
			// Data may only be excluded; the Include property is used to enable or disable the filter.
			{
				InheritanceTopicFilterModes modes = Modes;

				if (modes != InheritanceTopicFilterModes.None)
					Filter(element.Element, modes, Prefixes, IncludePrefixes);
			}

			// never match any elements so that they are all written back to the file, with the filter applied.
			return false;
		}

		public override ITopicFilter Clone()
		{
			return new InheritanceTopicFilter(this);
		}

		private static void Filter(IXPathNavigable element, InheritanceTopicFilterModes modes, ICollection<string> prefixes, bool includePrefixes)
		{
			XPathNavigator api = element.CreateNavigator();

			if (!api.GetAttribute("id", "").StartsWith("N:", StringComparison.OrdinalIgnoreCase) && api.MoveToChild("elements", ""))
			{
				if ((modes & InheritanceTopicFilterModes.Local) == InheritanceTopicFilterModes.Local)
				{
					if ((modes & InheritanceTopicFilterModes.Remote) == InheritanceTopicFilterModes.Remote)
					{
						if ((modes & InheritanceTopicFilterModes.Prefixes) == InheritanceTopicFilterModes.Prefixes && prefixes.Count > 0
							&& includePrefixes)		// since local and remote cover all elements, there's no reason to check prefixes if they won't be used to counter the local and remote filters.
							FilterWithPrefixes(api, prefixes, true, true, true);
						else
							FilterWithoutPrefixes(api, true, true);
					}
					else if ((modes & InheritanceTopicFilterModes.Prefixes) == InheritanceTopicFilterModes.Prefixes && prefixes.Count > 0)
						FilterWithPrefixes(api, prefixes, includePrefixes, true, false);
					else
						FilterWithoutPrefixes(api, true, false);
				}
				else if ((modes & InheritanceTopicFilterModes.Remote) == InheritanceTopicFilterModes.Remote)
				{
					if ((modes & InheritanceTopicFilterModes.Prefixes) == InheritanceTopicFilterModes.Prefixes && prefixes.Count > 0)
						FilterWithPrefixes(api, prefixes, includePrefixes, false, true);
					else
						FilterWithoutPrefixes(api, false, true);
				}
				else if ((modes & InheritanceTopicFilterModes.Prefixes) == InheritanceTopicFilterModes.Prefixes && prefixes.Count > 0
					&& !includePrefixes)		// if prefixes are to be included then nothing will be filtered at this point so there'd be no reason to process them
					FilterPrefixesOnly(api, prefixes);
			}
		}

		private static void FilterWithoutPrefixes(XPathNavigator elements, bool checkLocal, bool checkRemote)
		{
			foreach (XPathNavigator element in GetChildElements(elements))
				if (IsInherited(element, checkLocal, checkRemote))
					FilterElement(element);
		}

		private static void FilterWithPrefixes(XPathNavigator elements, ICollection<string> prefixes, bool includePrefixes, bool checkLocal, bool checkRemote)
		{
			if (includePrefixes)
			{
				foreach (XPathNavigator element in GetChildElements(elements))
					if (IsInherited(element, checkLocal, checkRemote) && !MatchesPrefix(element, prefixes))
						FilterElement(element);
			}
			else
			{
				foreach (XPathNavigator element in GetChildElements(elements))
					if (IsInherited(element, checkLocal, checkRemote) || MatchesPrefix(element, prefixes))
						FilterElement(element);
			}
		}

		private static void FilterPrefixesOnly(XPathNavigator elements, ICollection<string> prefixes)
		{
			foreach (XPathNavigator element in GetChildElements(elements))
			{
				if (MatchesPrefix(element, prefixes))
					FilterElement(element);
			}
		}

		private static bool MatchesPrefix(XPathNavigator element, ICollection<string> prefixes)
		{
			string id = element.GetAttribute("api", "");
			id = id.Substring(id.IndexOf(':') + 1);

			foreach (string prefix in prefixes)
				if (id.StartsWith(prefix, StringComparison.Ordinal))
					return true;

			return false;
		}

		private static void FilterElement(XPathNavigator element)
		{
			element.DeleteSelf();		// NOTE: calling DeleteSelf will move the navigator to the parent element

			if (string.Equals(element.LocalName, "element", StringComparison.Ordinal) && !element.HasChildren)
				// all child <element> tags of a method overloads element have been deleted, so the parent should also be deleted
				element.DeleteSelf();
		}

		private static IEnumerable<XPathNavigator> GetChildElements(XPathNavigator element)
		{
			// NOTE: do not use the yield statement since the underlying collection may change 
			// during this iteration.  (That's the reason for this method in the first place.)

			List<XPathNavigator> elements = new List<XPathNavigator>(128);

			foreach (XPathNavigator child in element.SelectChildren(XPathNodeType.Element))
			{
				XPathNodeIterator children = child.SelectChildren("element", "");

				if (children.Count > 0)
				{
					foreach (XPathNavigator subchild in children)
						elements.Add(subchild);
				}
				else
					elements.Add(child);
			}

			return elements;
		}

		private static bool IsInherited(XPathNavigator element, bool checkLocal, bool checkRemote)
		{
			if (checkRemote && element.HasChildren)
				// Assume that this member is inherited from a remote API since chidren are required to specify the API data.
				return true;
			else if (checkLocal && !element.HasChildren)
			// Assume that this member is a local API; chidren are not required since the API is already documented in the reflection file.
			{
				string api = element.GetAttribute("api", "");

				if (!string.IsNullOrEmpty(api))
				{
					string typeId = ReflectionApiElement.ParseBaseName(api);

					if (!string.IsNullOrEmpty(typeId) && !typeId.Equals(GetTypeId(element), StringComparison.Ordinal))
						return true;
				}
			}

			return false;
		}

		private static string GetTypeId(IXPathNavigable element)
		{
			XPathNavigator api = element.CreateNavigator();

			do
			{
				if (!api.MoveToParent())
					return null;
			}
			while (api.LocalName != "api");

			if (api.MoveToChild("topicdata", ""))
			{
				string id = api.GetAttribute("typeTopicId", "");

				if (!string.IsNullOrEmpty(id))
					return ReflectionApiElement.ParseFullName(id);
			}

			return null;
		}
		#endregion
	}
}